/**
 @file host.c
 @brief ENet host management functions
*/
#define ENET_BUILDING_LIB 1
#include <string.h>
#include "enet.h"

/** @defgroup host ENet host functions
	@{
*/

/** Creates a host for communicating to peers.

	@param address   the address at which other peers may connect to this host.  If NULL, then no peers may connect to the host.
	@param peerCount the maximum number of peers that should be allocated for the host.
	@param incomingBandwidth downstream bandwidth of the host in bytes/second; if 0, ENet will assume unlimited bandwidth.
	@param outgoingBandwidth upstream bandwidth of the host in bytes/second; if 0, ENet will assume unlimited bandwidth.

	@returns the host on success and NULL on failure

	@remarks ENet will strategically drop packets on specific sides of a connection between hosts
	to ensure the host's bandwidth is not overwhelmed.  The bandwidth parameters also determine
	the window size of a connection which limits the amount of reliable packets that may be in transit
	at any given time.
*/
ENetHost *
enet_host_create(const ENetAddress * address, size_t peerCount, enet_uint32 incomingBandwidth, enet_uint32 outgoingBandwidth)
{
	ENetHost * host;
	ENetPeer * currentPeer;

	if (peerCount > ENET_PROTOCOL_MAXIMUM_PEER_ID)
		return NULL;

	host = (ENetHost *)enet_malloc(sizeof(ENetHost));
	if (host == NULL)
		return NULL;

	host->peers = (ENetPeer *)enet_malloc(peerCount * sizeof(ENetPeer));
	if (host->peers == NULL)
	{
		enet_free(host);

		return NULL;
	}
	memset(host->peers, 0, peerCount * sizeof(ENetPeer));

	host->socket = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
	if (host->socket == ENET_SOCKET_NULL || (address != NULL && enet_socket_bind(host->socket, address) < 0))
	{
		if (host->socket != ENET_SOCKET_NULL)
			enet_socket_destroy(host->socket);

		enet_free(host->peers);
		enet_free(host);

		return NULL;
	}

	enet_socket_set_option(host->socket, ENET_SOCKOPT_NONBLOCK, 1);
	enet_socket_set_option(host->socket, ENET_SOCKOPT_BROADCAST, 1);
	enet_socket_set_option(host->socket, ENET_SOCKOPT_RCVBUF, ENET_HOST_RECEIVE_BUFFER_SIZE);
	enet_socket_set_option(host->socket, ENET_SOCKOPT_SNDBUF, ENET_HOST_SEND_BUFFER_SIZE);

	if (address != NULL)
		host->address = *address;

	host->channelLimit = ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT;
	host->incomingBandwidth = incomingBandwidth;
	host->outgoingBandwidth = outgoingBandwidth;
	host->bandwidthThrottleEpoch = 0;
	host->recalculateBandwidthLimits = 0;
	host->mtu = ENET_HOST_DEFAULT_MTU;
	host->peerCount = peerCount;
	host->commandCount = 0;
	host->bufferCount = 0;
	host->checksum = NULL;
	host->receivedAddress.host = ENET_HOST_ANY;
	host->receivedAddress.port = 0;
	host->receivedDataLength = 0;

	host->totalSentData = 0;
	host->totalSentPackets = 0;
	host->totalReceivedData = 0;
	host->totalReceivedPackets = 0;

	enet_list_clear(&host->dispatchQueue);

	for (currentPeer = host->peers;
		currentPeer < &host->peers[host->peerCount];
		++currentPeer)
	{
		currentPeer->host = host;
		currentPeer->incomingPeerID = currentPeer - host->peers;
		currentPeer->data = NULL;

		enet_list_clear(&currentPeer->acknowledgements);
		enet_list_clear(&currentPeer->sentReliableCommands);
		enet_list_clear(&currentPeer->sentUnreliableCommands);
		enet_list_clear(&currentPeer->outgoingReliableCommands);
		enet_list_clear(&currentPeer->outgoingUnreliableCommands);
		enet_list_clear(&currentPeer->dispatchedCommands);

		enet_peer_reset(currentPeer);
	}

	return host;
}

/** Destroys the host and all resources associated with it.
	@param host pointer to the host to destroy
*/
void
enet_host_destroy(ENetHost * host)
{
	ENetPeer * currentPeer;

	enet_socket_destroy(host->socket);

	for (currentPeer = host->peers;
		currentPeer < &host->peers[host->peerCount];
		++currentPeer)
	{
		enet_peer_reset(currentPeer);
	}

	enet_free(host->peers);
	enet_free(host);
}

/** Initiates a connection to a foreign host.
	@param host host seeking the connection
	@param address destination for the connection
	@param channelCount number of channels to allocate
	@returns a peer representing the foreign host on success, NULL on failure
	@remarks The peer returned will have not completed the connection until enet_host_service()
	notifies of an ENET_EVENT_TYPE_CONNECT event for the peer.
*/
ENetPeer *
enet_host_connect(ENetHost * host, const ENetAddress * address, size_t channelCount)
{
	ENetPeer * currentPeer;
	ENetChannel * channel;
	ENetProtocol command;

	if (channelCount < ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT)
		channelCount = ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT;
	else
		if (channelCount > ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT)
			channelCount = ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT;

	for (currentPeer = host->peers;
		currentPeer < &host->peers[host->peerCount];
		++currentPeer)
	{
		if (currentPeer->state == ENET_PEER_STATE_DISCONNECTED)
			break;
	}

	if (currentPeer >= &host->peers[host->peerCount])
		return NULL;

	currentPeer->channels = (ENetChannel *)enet_malloc(channelCount * sizeof(ENetChannel));
	if (currentPeer->channels == NULL)
		return NULL;
	currentPeer->channelCount = channelCount;
	currentPeer->state = ENET_PEER_STATE_CONNECTING;
	currentPeer->address = *address;
	currentPeer->sessionID = (enet_uint32)enet_rand();

	if (host->outgoingBandwidth == 0)
		currentPeer->windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE;
	else
		currentPeer->windowSize = (host->outgoingBandwidth /
			ENET_PEER_WINDOW_SIZE_SCALE) *
		ENET_PROTOCOL_MINIMUM_WINDOW_SIZE;

	if (currentPeer->windowSize < ENET_PROTOCOL_MINIMUM_WINDOW_SIZE)
		currentPeer->windowSize = ENET_PROTOCOL_MINIMUM_WINDOW_SIZE;
	else
		if (currentPeer->windowSize > ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE)
			currentPeer->windowSize = ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE;

	for (channel = currentPeer->channels;
		channel < &currentPeer->channels[channelCount];
		++channel)
	{
		channel->outgoingReliableSequenceNumber = 0;
		channel->outgoingUnreliableSequenceNumber = 0;
		channel->incomingReliableSequenceNumber = 0;
		channel->incomingUnreliableSequenceNumber = 0;

		enet_list_clear(&channel->incomingReliableCommands);
		enet_list_clear(&channel->incomingUnreliableCommands);

		channel->usedReliableWindows = 0;
		memset(channel->reliableWindows, 0, sizeof(channel->reliableWindows));
	}

	command.header.command = ENET_PROTOCOL_COMMAND_CONNECT | ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE;
	command.header.channelID = 0xFF;
	command.connect.outgoingPeerID = ENET_HOST_TO_NET_16(currentPeer->incomingPeerID);
	command.connect.mtu = ENET_HOST_TO_NET_16(currentPeer->mtu);
	command.connect.windowSize = ENET_HOST_TO_NET_32(currentPeer->windowSize);
	command.connect.channelCount = ENET_HOST_TO_NET_32(channelCount);
	command.connect.incomingBandwidth = ENET_HOST_TO_NET_32(host->incomingBandwidth);
	command.connect.outgoingBandwidth = ENET_HOST_TO_NET_32(host->outgoingBandwidth);
	command.connect.packetThrottleInterval = ENET_HOST_TO_NET_32(currentPeer->packetThrottleInterval);
	command.connect.packetThrottleAcceleration = ENET_HOST_TO_NET_32(currentPeer->packetThrottleAcceleration);
	command.connect.packetThrottleDeceleration = ENET_HOST_TO_NET_32(currentPeer->packetThrottleDeceleration);
	command.connect.sessionID = currentPeer->sessionID;

	enet_peer_queue_outgoing_command(currentPeer, &command, NULL, 0, 0);

	return currentPeer;
}

/** Limits the maximum allowed channels of future incoming connections.
	@param host host to limit
	@param channelLimit the maximum number of channels allowed; if 0, then this is equivalent to ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT
*/
void
enet_host_channel_limit(ENetHost * host, size_t channelLimit)
{
	if (!channelLimit || channelLimit > ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT)
		channelLimit = ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT;
	else
		if (channelLimit < ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT)
			channelLimit = ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT;

	host->channelLimit = channelLimit;
}

/** Queues a packet to be sent to all peers associated with the host.
	@param host host on which to broadcast the packet
	@param channelID channel on which to broadcast
	@param packet packet to broadcast
*/
void
enet_host_broadcast(ENetHost * host, enet_uint8 channelID, ENetPacket * packet)
{
	ENetPeer * currentPeer;

	for (currentPeer = host->peers;
		currentPeer < &host->peers[host->peerCount];
		++currentPeer)
	{
		if (currentPeer->state != ENET_PEER_STATE_CONNECTED)
			continue;

		enet_peer_send(currentPeer, channelID, packet);
	}

	if (packet->referenceCount == 0)
		enet_packet_destroy(packet);
}

/** Adjusts the bandwidth limits of a host.
	@param host host to adjust
	@param incomingBandwidth new incoming bandwidth
	@param outgoingBandwidth new outgoing bandwidth
	@remarks the incoming and outgoing bandwidth parameters are identical in function to those
	specified in enet_host_create().
*/
void
enet_host_bandwidth_limit(ENetHost * host, enet_uint32 incomingBandwidth, enet_uint32 outgoingBandwidth)
{
	host->incomingBandwidth = incomingBandwidth;
	host->outgoingBandwidth = outgoingBandwidth;
	host->recalculateBandwidthLimits = 1;
}

void
enet_host_bandwidth_throttle(ENetHost * host)
{
	enet_uint32 timeCurrent = enet_time_get(),
		elapsedTime = timeCurrent - host->bandwidthThrottleEpoch,
		peersTotal = 0,
		dataTotal = 0,
		peersRemaining,
		bandwidth,
		throttle = 0,
		bandwidthLimit = 0;
	int needsAdjustment;
	ENetPeer * peer;
	ENetProtocol command;

	if (elapsedTime < ENET_HOST_BANDWIDTH_THROTTLE_INTERVAL)
		return;

	for (peer = host->peers;
		peer < &host->peers[host->peerCount];
		++peer)
	{
		if (peer->state != ENET_PEER_STATE_CONNECTED && peer->state != ENET_PEER_STATE_DISCONNECT_LATER)
			continue;

		++peersTotal;
		dataTotal += peer->outgoingDataTotal;
	}

	if (peersTotal == 0)
		return;

	peersRemaining = peersTotal;
	needsAdjustment = 1;

	if (host->outgoingBandwidth == 0)
		bandwidth = ~0;
	else
		bandwidth = (host->outgoingBandwidth * elapsedTime) / 1000;

	while (peersRemaining > 0 && needsAdjustment != 0)
	{
		needsAdjustment = 0;

		if (dataTotal < bandwidth)
			throttle = ENET_PEER_PACKET_THROTTLE_SCALE;
		else
			throttle = (bandwidth * ENET_PEER_PACKET_THROTTLE_SCALE) / dataTotal;

		for (peer = host->peers;
			peer < &host->peers[host->peerCount];
			++peer)
		{
			enet_uint32 peerBandwidth;

			if ((peer->state != ENET_PEER_STATE_CONNECTED && peer->state != ENET_PEER_STATE_DISCONNECT_LATER) ||
				peer->incomingBandwidth == 0 ||
				peer->outgoingBandwidthThrottleEpoch == timeCurrent)
				continue;

			peerBandwidth = (peer->incomingBandwidth * elapsedTime) / 1000;
			if ((throttle * peer->outgoingDataTotal) / ENET_PEER_PACKET_THROTTLE_SCALE <= peerBandwidth)
				continue;

			peer->packetThrottleLimit = (peerBandwidth *
				ENET_PEER_PACKET_THROTTLE_SCALE) / peer->outgoingDataTotal;

			if (peer->packetThrottleLimit == 0)
				peer->packetThrottleLimit = 1;

			if (peer->packetThrottle > peer->packetThrottleLimit)
				peer->packetThrottle = peer->packetThrottleLimit;

			peer->outgoingBandwidthThrottleEpoch = timeCurrent;

			needsAdjustment = 1;
			--peersRemaining;
			bandwidth -= peerBandwidth;
			dataTotal -= peerBandwidth;
		}
	}

	if (peersRemaining > 0)
		for (peer = host->peers;
			peer < &host->peers[host->peerCount];
			++peer)
	{
		if ((peer->state != ENET_PEER_STATE_CONNECTED && peer->state != ENET_PEER_STATE_DISCONNECT_LATER) ||
			peer->outgoingBandwidthThrottleEpoch == timeCurrent)
			continue;

		peer->packetThrottleLimit = throttle;

		if (peer->packetThrottle > peer->packetThrottleLimit)
			peer->packetThrottle = peer->packetThrottleLimit;
	}

	if (host->recalculateBandwidthLimits)
	{
		host->recalculateBandwidthLimits = 0;

		peersRemaining = peersTotal;
		bandwidth = host->incomingBandwidth;
		needsAdjustment = 1;

		if (bandwidth == 0)
			bandwidthLimit = 0;
		else
			while (peersRemaining > 0 && needsAdjustment != 0)
			{
				needsAdjustment = 0;
				bandwidthLimit = bandwidth / peersRemaining;

				for (peer = host->peers;
					peer < &host->peers[host->peerCount];
					++peer)
				{
					if ((peer->state != ENET_PEER_STATE_CONNECTED && peer->state != ENET_PEER_STATE_DISCONNECT_LATER) ||
						peer->incomingBandwidthThrottleEpoch == timeCurrent)
						continue;

					if (peer->outgoingBandwidth > 0 &&
						peer->outgoingBandwidth >= bandwidthLimit)
						continue;

					peer->incomingBandwidthThrottleEpoch = timeCurrent;

					needsAdjustment = 1;
					--peersRemaining;
					bandwidth -= peer->outgoingBandwidth;
				}
			}

		for (peer = host->peers;
			peer < &host->peers[host->peerCount];
			++peer)
		{
			if (peer->state != ENET_PEER_STATE_CONNECTED && peer->state != ENET_PEER_STATE_DISCONNECT_LATER)
				continue;

			command.header.command = ENET_PROTOCOL_COMMAND_BANDWIDTH_LIMIT | ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE;
			command.header.channelID = 0xFF;
			command.bandwidthLimit.outgoingBandwidth = ENET_HOST_TO_NET_32(host->outgoingBandwidth);

			if (peer->incomingBandwidthThrottleEpoch == timeCurrent)
				command.bandwidthLimit.incomingBandwidth = ENET_HOST_TO_NET_32(peer->outgoingBandwidth);
			else
				command.bandwidthLimit.incomingBandwidth = ENET_HOST_TO_NET_32(bandwidthLimit);

			enet_peer_queue_outgoing_command(peer, &command, NULL, 0, 0);
		}
	}

	host->bandwidthThrottleEpoch = timeCurrent;

	for (peer = host->peers;
		peer < &host->peers[host->peerCount];
		++peer)
	{
		peer->incomingDataTotal = 0;
		peer->outgoingDataTotal = 0;
	}
}

/** @} */